From 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b Mon Sep 17 00:00:00 2001 From: Fuwn <50817549+Fuwn@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:09:50 +0000 Subject: Initial commit Created from https://vercel.com/new --- .../[websiteId]/sessions/SessionActivity.tsx | 94 +++++++++++++++++++++ .../websites/[websiteId]/sessions/SessionData.tsx | 32 +++++++ .../websites/[websiteId]/sessions/SessionInfo.tsx | 85 +++++++++++++++++++ .../websites/[websiteId]/sessions/SessionModal.tsx | 41 +++++++++ .../[websiteId]/sessions/SessionProfile.tsx | 84 +++++++++++++++++++ .../[websiteId]/sessions/SessionProperties.tsx | 97 ++++++++++++++++++++++ .../websites/[websiteId]/sessions/SessionStats.tsx | 21 +++++ .../[websiteId]/sessions/SessionsDataTable.tsx | 15 ++++ .../[websiteId]/sessions/SessionsMetricsBar.tsx | 40 +++++++++ .../websites/[websiteId]/sessions/SessionsPage.tsx | 43 ++++++++++ .../[websiteId]/sessions/SessionsTable.tsx | 58 +++++++++++++ .../(main)/websites/[websiteId]/sessions/page.tsx | 12 +++ 12 files changed, 622 insertions(+) create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx create mode 100644 src/app/(main)/websites/[websiteId]/sessions/page.tsx (limited to 'src/app/(main)/websites/[websiteId]/sessions') diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx new file mode 100644 index 0000000..cbb2810 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionActivity.tsx @@ -0,0 +1,94 @@ +import { + Button, + Column, + Dialog, + DialogTrigger, + Heading, + Icon, + Popover, + Row, + StatusLight, + Text, +} from '@umami/react-zen'; +import { isSameDay } from 'date-fns'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useMobile, useSessionActivityQuery, useTimezone } from '@/components/hooks'; +import { Eye, FileText } from '@/components/icons'; +import { EventData } from '@/components/metrics/EventData'; +import { Lightning } from '@/components/svg'; + +export function SessionActivity({ + websiteId, + sessionId, + startDate, + endDate, +}: { + websiteId: string; + sessionId: string; + startDate: Date; + endDate: Date; +}) { + const { formatMessage, labels } = useMessages(); + const { formatTimezoneDate } = useTimezone(); + const { data, isLoading, error } = useSessionActivityQuery( + websiteId, + sessionId, + startDate, + endDate, + ); + const { isMobile } = useMobile(); + let lastDay = null; + + return ( + + + {data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => { + const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt)); + lastDay = createdAt; + + return ( + + {showHeader && {formatTimezoneDate(createdAt, 'PPPP')}} + + + {formatTimezoneDate(createdAt, 'pp')} + + + {eventName ? : } + + {eventName + ? formatMessage(labels.triggeredEvent) + : formatMessage(labels.viewedPage)} + + + {eventName || urlPath} + + {hasData > 0 && } + + + + ); + })} + + + ); +} + +const PropertiesButton = props => { + return ( + + + + + + + + + ); +}; diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx new file mode 100644 index 0000000..7c82c17 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionData.tsx @@ -0,0 +1,32 @@ +import { Box, Column, Label, Row, Text } from '@umami/react-zen'; +import { Empty } from '@/components/common/Empty'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useSessionDataQuery } from '@/components/hooks'; +import { DATA_TYPES } from '@/lib/constants'; + +export function SessionData({ websiteId, sessionId }: { websiteId: string; sessionId: string }) { + const { data, isLoading, error } = useSessionDataQuery(websiteId, sessionId); + + return ( + + {!data?.length && } + + {data?.map(({ dataKey, dataType, stringValue }) => { + return ( + + + + {stringValue} + + + {DATA_TYPES[dataType]} + + + + + ); + })} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx new file mode 100644 index 0000000..f15e6ee --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionInfo.tsx @@ -0,0 +1,85 @@ +import { Column, Grid, Icon, Label, Row } from '@umami/react-zen'; +import type { ReactNode } from 'react'; +import { DateDistance } from '@/components/common/DateDistance'; +import { TypeIcon } from '@/components/common/TypeIcon'; +import { useFormat, useLocale, useMessages, useRegionNames } from '@/components/hooks'; +import { Calendar, KeyRound, Landmark, MapPin } from '@/components/icons'; + +export function SessionInfo({ data }) { + const { locale } = useLocale(); + const { formatMessage, labels } = useMessages(); + const { formatValue } = useFormat(); + const { getRegionName } = useRegionNames(locale); + + return ( + + }> + {data?.distinctId} + + + }> + + + + }> + + + + } + > + {formatValue(data?.country, 'country')} + + + }> + {getRegionName(data?.region)} + + + }> + {data?.city} + + + } + > + {formatValue(data?.browser, 'browser')} + + + } + > + {formatValue(data?.os, 'os')} + + + } + > + {formatValue(data?.device, 'device')} + + + ); +} + +const Info = ({ + label, + icon, + children, +}: { + label: string; + icon?: ReactNode; + children: ReactNode; +}) => { + return ( + + + + {icon && {icon}} + {children || '—'} + + + ); +}; diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx new file mode 100644 index 0000000..d658038 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionModal.tsx @@ -0,0 +1,41 @@ +import { Column, Dialog, Modal, type ModalProps } from '@umami/react-zen'; +import { SessionProfile } from '@/app/(main)/websites/[websiteId]/sessions/SessionProfile'; +import { useNavigation } from '@/components/hooks'; + +export interface SessionModalProps extends ModalProps { + websiteId: string; +} + +export function SessionModal({ websiteId, ...props }: SessionModalProps) { + const { + router, + query: { session }, + updateParams, + } = useNavigation(); + const handleOpenChange = (isOpen: boolean) => { + if (!isOpen) { + router.push(updateParams({ session: undefined })); + } + }; + + return ( + + + + {({ close }) => ( + + close()} /> + + )} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx new file mode 100644 index 0000000..6624d43 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProfile.tsx @@ -0,0 +1,84 @@ +import { + Button, + Column, + Icon, + Row, + Tab, + TabList, + TabPanel, + Tabs, + TextField, +} from '@umami/react-zen'; +import { X } from 'lucide-react'; +import { Avatar } from '@/components/common/Avatar'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages, useWebsiteSessionQuery } from '@/components/hooks'; +import { SessionActivity } from './SessionActivity'; +import { SessionData } from './SessionData'; +import { SessionInfo } from './SessionInfo'; +import { SessionStats } from './SessionStats'; + +export function SessionProfile({ + websiteId, + sessionId, + onClose, +}: { + websiteId: string; + sessionId: string; + onClose?: () => void; +}) { + const { data, isLoading, error } = useWebsiteSessionQuery(websiteId, sessionId); + const { formatMessage, labels } = useMessages(); + + return ( + + {data && ( + + {onClose && ( + + + + )} + + + + + + + + + + + + + {formatMessage(labels.activity)} + {formatMessage(labels.properties)} + + + + + + + + + + + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx new file mode 100644 index 0000000..1693d05 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionProperties.tsx @@ -0,0 +1,97 @@ +import { Column, Grid, ListItem, Select } from '@umami/react-zen'; +import { useMemo, useState } from 'react'; +import { PieChart } from '@/components/charts/PieChart'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { + useMessages, + useSessionDataPropertiesQuery, + useSessionDataValuesQuery, +} from '@/components/hooks'; +import { ListTable } from '@/components/metrics/ListTable'; +import { CHART_COLORS } from '@/lib/constants'; + +export function SessionProperties({ websiteId }: { websiteId: string }) { + const [propertyName, setPropertyName] = useState(''); + const { formatMessage, labels } = useMessages(); + const { data, isLoading, isFetching, error } = useSessionDataPropertiesQuery(websiteId); + + const properties: string[] = data?.map(e => e.propertyName); + + return ( + + + {data && ( + + + + )} + {propertyName && } + + + ); +} + +const SessionValues = ({ websiteId, propertyName }) => { + const { data, isLoading, isFetching, error } = useSessionDataValuesQuery(websiteId, propertyName); + + const propertySum = useMemo(() => { + return data?.reduce((sum, { total }) => sum + total, 0) ?? 0; + }, [data]); + + const chartData = useMemo(() => { + if (!propertyName || !data) return null; + return { + labels: data.map(({ value }) => value), + datasets: [ + { + data: data.map(({ total }) => total), + backgroundColor: CHART_COLORS, + borderWidth: 0, + }, + ], + }; + }, [propertyName, data]); + + const tableData = useMemo(() => { + if (!propertyName || !data || propertySum === 0) return []; + return data.map(({ value, total }) => ({ + label: value, + count: total, + percent: 100 * (total / propertySum), + })); + }, [propertyName, data, propertySum]); + + return ( + + {data && ( + + + + + )} + + ); +}; diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx new file mode 100644 index 0000000..e25be9a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionStats.tsx @@ -0,0 +1,21 @@ +import { useMessages } from '@/components/hooks'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { formatShortTime } from '@/lib/format'; + +export function SessionStats({ data }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} + /> + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx new file mode 100644 index 0000000..b1b9f65 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsDataTable.tsx @@ -0,0 +1,15 @@ +import { DataGrid } from '@/components/common/DataGrid'; +import { useWebsiteSessionsQuery } from '@/components/hooks'; +import { SessionsTable } from './SessionsTable'; + +export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) { + const queryResult = useWebsiteSessionsQuery(websiteId); + + return ( + + {({ data }) => { + return ; + }} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx new file mode 100644 index 0000000..c8317a2 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsMetricsBar.tsx @@ -0,0 +1,40 @@ +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { useMessages } from '@/components/hooks'; +import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { formatLongNumber } from '@/lib/format'; + +export function SessionsMetricsBar({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + const { data, isLoading, isFetching, error } = useWebsiteSessionStatsQuery(websiteId); + + return ( + + {data && ( + + + + + + + )} + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx new file mode 100644 index 0000000..8e9d2f2 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsPage.tsx @@ -0,0 +1,43 @@ +'use client'; +import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen'; +import { type Key, useState } from 'react'; +import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { Panel } from '@/components/common/Panel'; +import { useMessages } from '@/components/hooks'; +import { getItem, setItem } from '@/lib/storage'; +import { SessionProperties } from './SessionProperties'; +import { SessionsDataTable } from './SessionsDataTable'; + +const KEY_NAME = 'umami.sessions.tab'; + +export function SessionsPage({ websiteId }) { + const [tab, setTab] = useState(getItem(KEY_NAME) || 'activity'); + const { formatMessage, labels } = useMessages(); + + const handleSelect = (value: Key) => { + setItem(KEY_NAME, value); + setTab(value); + }; + + return ( + + + + + + {formatMessage(labels.activity)} + {formatMessage(labels.properties)} + + + + + + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx new file mode 100644 index 0000000..5d3bb37 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/SessionsTable.tsx @@ -0,0 +1,58 @@ +import { DataColumn, DataTable, type DataTableProps } from '@umami/react-zen'; +import Link from 'next/link'; +import { Avatar } from '@/components/common/Avatar'; +import { DateDistance } from '@/components/common/DateDistance'; +import { TypeIcon } from '@/components/common/TypeIcon'; +import { useFormat, useMessages, useNavigation } from '@/components/hooks'; + +export function SessionsTable(props: DataTableProps) { + const { formatMessage, labels } = useMessages(); + const { formatValue } = useFormat(); + const { updateParams } = useNavigation(); + + return ( + + + {(row: any) => ( + + + + )} + + + + + {(row: any) => ( + + {formatValue(row.country, 'country')} + + )} + + + + {(row: any) => ( + + {formatValue(row.browser, 'browser')} + + )} + + + {(row: any) => ( + + {formatValue(row.os, 'os')} + + )} + + + {(row: any) => ( + + {formatValue(row.device, 'device')} + + )} + + + {(row: any) => } + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/sessions/page.tsx b/src/app/(main)/websites/[websiteId]/sessions/page.tsx new file mode 100644 index 0000000..221ab71 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/sessions/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { SessionsPage } from './SessionsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Sessions', +}; -- cgit v1.2.3